package anonymouschat.service;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;

import javax.crypto.SecretKey;

import anonymouschat.exceptions.DecryptionException;
import anonymouschat.exceptions.InvalidTagException;
import anonymouschat.exceptions.LoadPrivateIdentityException;
import anonymouschat.messages.EncryptedMessageWrapperMessage;
import anonymouschat.security.KeySalt;
import anonymouschat.security.Keys;
import anonymouschat.security.SecurityUtils;
import anonymouschat.utils.LocalFileOps;
import anonymouschat.utils.Print;

/**
 * 
 * A class to store public keys, and (optionally) private keys
 * 
 */
public class ServerIdentObject implements Serializable {

	private static final long serialVersionUID = -2636702683734153948L;

	private PrivateKey privateKey;

	private SecretKey symmetricKey = null;

	private PublicKey publicKey;

	private PublicKey publicSigningKey;

	private PrivateKey privateSigningKey;

	private String serverName;

	private transient Server server;

	/**
	 * Given server parameters, generate RSA keys, signing keys, and aes/mac
	 * file keys
	 * 
	 * @param svrName
	 *            the name of the server
	 * @param password
	 *            the password to use for protecting private keys
	 * @param newSalt
	 *            salt to use with a password
	 * @param server
	 *            a handle to the server creating this object
	 * @throws NoSuchAlgorithmException
	 */
	public ServerIdentObject(String svrName, String password, KeySalt newSalt,
			Server server) throws NoSuchAlgorithmException {
		// Generate private and public key here
		Print.debug("Generating new identity file for server: " + svrName);
		KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
		gen.initialize(3072);
		KeyPair keyPair = gen.generateKeyPair();
		SecretKey secret = Keys.createAESKey(password, newSalt.getSalt1Bytes());
		SecretKey secret2 = Keys
				.createAESKey(password, newSalt.getSalt2Bytes());
		publicKey = keyPair.getPublic();
		privateKey = keyPair.getPrivate();
		KeyPair signingKeyPair = gen.generateKeyPair();
		publicSigningKey = signingKeyPair.getPublic();
		privateSigningKey = signingKeyPair.getPrivate();
		serverName = svrName;

		this.server = server;
		saveFullIdentity(secret, secret2, newSalt); // Save the identity to
													// file so next time we
													// can use it.
	}

	public void setSymmetricKey(SecretKey key) {
		symmetricKey = key;
	}

	public SecretKey getSymmetricKey() {
		return symmetricKey;
	}

	/**
	 * Load an existing identity given the name and password
	 * 
	 * @param svrName
	 *            the name of the server
	 * @return &ltServerIdentObject&gt a server identity
	 * @throws LoadPrivateIdentityException
	 */
	public static ServerIdentObject loadPrivateIdentity(String svrName,
			String password) throws LoadPrivateIdentityException {
		Print.debug("Loading existing identity: " + svrName);
		ServerIdentObject ident = null;
		File identityFile = null;
		File saltFile = null;
		identityFile = new File(
				LocalFileOps.getPrivateIdentityFileName(svrName));
		saltFile = new File(LocalFileOps.getSaltFileName(svrName));
		try {
			try {
				long idLength = identityFile.length();
				byte[] encData = new byte[(int) idLength];
				FileInputStream fin = new FileInputStream(identityFile);
				fin.read(encData);
				fin.close();

				fin = new FileInputStream(saltFile);
				ObjectInputStream ois = new ObjectInputStream(fin);
				Object temp = ois.readObject();
				KeySalt salt = null;
				if (temp instanceof KeySalt) {
					salt = (KeySalt) temp;
				} else {
					Print.error("Unexpected object type in salt file");
					LocalFileOps.appendAuditFile("Failed to launch server "
							+ svrName + " the salt file is corrupted.");
					System.exit(1);
				}
				ois.close();
				fin.close();
				temp = null;

				SecretKey secret = Keys.createAESKey(password,
						salt.getSalt1Bytes());
				SecretKey secret2 = Keys.createAESKey(password,
						salt.getSalt2Bytes());
				password = null;
				Print.debug("Decrypting " + encData.length + " bytes");
				temp = LocalFileOps.readMacThenEncObject(secret, secret2, salt,
						LocalFileOps.getPrivateIdentityFileName(svrName));

				if (temp instanceof ServerEntry) {
					ServerEntry entry = (ServerEntry) temp;
					ident = (ServerIdentObject) entry.serverIdent;
				} else {
					Print.error("Unexpected object type in identity file");
					System.exit(1);
				}

				identityFile = null;
			} catch (FileNotFoundException e) {
				Print.error("Cannot find identity file", e);
				throw new LoadPrivateIdentityException(
						"Could not load identity file: "
								+ e.getLocalizedMessage());
			} catch (IOException e) {
				Print.error("I/O error opening iSdentity file", e);
				throw new LoadPrivateIdentityException(
						"Unable to read serialized server identity object from identity file: "
								+ e.getLocalizedMessage());
			} catch (ClassNotFoundException e) {
				Print.error("Failed to load salt file ", e);
				throw new LoadPrivateIdentityException(
						"Unable to read server salt file");
			} catch (InvalidTagException e) {
				Print.error("Could not load server identity file because the MAC tag didn't match");
				throw new LoadPrivateIdentityException(
						"The MAC tag didn't match");
			} catch (DecryptionException e) {
				Print.error("Could not load server identity file because the password was incorrect.");
				throw new LoadPrivateIdentityException(
						"The password was incorrect.");
			}
		} catch (LoadPrivateIdentityException up) {
			LocalFileOps.appendAuditFile("Failed to load server file: "
					+ up.getDescription());
			throw up;
		}
		return ident;

	}

	public PublicKey getPublicKey() {
		return publicKey;
	}

	public PrivateKey getPrivateKey() {
		return privateKey;
	}

	public PublicKey getPublicSigningKey() {
		return publicSigningKey;
	}

	public PrivateKey getPrivateSigningKey() {
		return privateSigningKey;
	}

	/**
	 * Removes the outer encryption layer of an EncryptedMessageWrapperMessage
	 * 
	 * @param encryptedMessage
	 *            the nested message that you want to remove the outer layer of
	 * @return &lt Object &gt the inner object that was encrypted
	 * @throws DecryptionException
	 */
	public Object unwrapEncryptionLayer(
			EncryptedMessageWrapperMessage encryptedMessage)
			throws DecryptionException {

		// Decrypts, re-serializes message object.
		byte[] encryptedByteArray = encryptedMessage.getencryptedMessage();
		byte[] unencryptedByteArray = null;

		if (encryptedMessage.isAsymmetricEncrypted()) {
			unencryptedByteArray = SecurityUtils.decRSA(encryptedByteArray,
					privateKey);
		} else {
			unencryptedByteArray = SecurityUtils.decAES(encryptedByteArray,
					symmetricKey, encryptedMessage.getIV().getIV());

		}

		return SecurityUtils.byteArrayToObject(unencryptedByteArray);
	}

	/**
	 * Save the private encryption key, the signing key, salt, iv, etc
	 * 
	 * @param encKey
	 *            the key to encrypt with
	 * @param macKey
	 *            the key to MAC with
	 * @param newSalt
	 *            the salt used for the IV
	 */
	public void saveFullIdentity(SecretKey encKey, SecretKey macKey,
			KeySalt newSalt) {
		try {
			ServerEntry privateID = new ServerEntry(this);
			Print.debug("Wrote public identity file");
			savePublicIdentity(this.serverName, this.server.getServerPort(),
					this.publicKey);

			LocalFileOps.writeMacThenEncObject(privateID, encKey, macKey,
					newSalt.getIv(),
					LocalFileOps.getPrivateIdentityFileName(serverName));

			Print.debug("Successfully wrote private identity file");

			LocalFileOps.write(newSalt,
					LocalFileOps.getSaltFileName(serverName));

			Print.debug("Wrote salt file");

		} catch (Exception e) {
			Print.error("Cannot save identities for " + serverName, e);
		}
	}

	/**
	 * Save the public portion of an identity
	 * 
	 * @param serverName
	 *            the friendly name of the server
	 * @param serverPort
	 *            the port the server listens on
	 * @param pubKey
	 *            the servers public keys
	 * @throws UnknownHostException
	 */
	public void savePublicIdentity(String serverName, int serverPort,
			PublicKey pubKey) throws UnknownHostException {
		Print.debug("Storing server identity: " + serverName + "@"
				+ InetAddress.getLocalHost().getHostAddress() + "/"
				+ serverPort);
		ServerEntry publicID = new ServerEntry(serverName, InetAddress
				.getLocalHost().getHostAddress(), serverPort, pubKey);

		// store the public ID
		LocalFileOps.write(publicID,
				LocalFileOps.getPublicIdentityFileName(serverName));

	}

}
